<?php

class Controller {

    const ALL_ACTIONS = '__ALL__';
    const DEFAULT_LAYOUT = '__DEFAULT_LAYOUT__';

    protected $before_filters = array();
    protected $after_filters = array();
    protected $layout = array(); // the layout template file path
    protected $rendered = false; // did we already call the render?
    protected $action_output; // the rendered contents from processing the action

    public function __construct() {
        $this->layout[self::DEFAULT_LAYOUT] = $this->get_default_layout();
    }

    public function __call($name, $arguments) {
        throw new Exception("action $name doesn't exist in controller: ".get_class($this));
    }

    protected function append_before_filter($filterName, $actions = self::ALL_ACTIONS) {
        array_push($this->before_filters, array($filterName, $actions));
    }

    protected function prepend_after_filter($filterName, $actions = self::ALL_ACTIONS) {
        array_unshift($this->after_filters, array($filterName, $actions));
    }

    protected function append_around_filter($filterName, $actions = self::ALL_ACTIONS) {
        array_push($this->before_filters, array($filterName, $actions));
        array_unshift($this->after_filters, array($filterName, $actions));
    }

    protected function process_before_filters($action, $params) {
        foreach ($this->before_filters as $filter) {
            $filterName = $filter[0];
            $actions = $filter[1];
            if ($actions == self::ALL_ACTIONS || in_array($action, $actions)) {
                $this->$filterName($params);
            }
        }
    }

    protected function process_after_filters($action, $params) {
        foreach ($this->after_filters as $filter) {
            $filterName = $filter[0];
            $actions = $filter[1];
            if ($actions == self::ALL_ACTIONS || in_array($action, $actions)) {
                $this->$filterName($params);
            }
        }
    }

    public function process($action, $params) {
        ob_start();
        $this->process_before_filters($action, $params);
        $this->$action($params);
        $this->process_after_filters($action, $params);
        $this->action_output = ob_get_contents();
        ob_end_clean(); // prevent any content to be sent to the browser, this is important since we want the hooks to function properly.
    }

    /**
     * By default, if this method is not invoked by the controller itself,
     * (which means it was invoked by response object), then it will render
     * the default template if it exists, and with the layout if it was specified.
     *
     * If controller has invoked the render function itself, then we will
     * honor the $args passed in by controller itself.
     *
     * @param string $action
     * @param array $args
     * @return void
     */
    public function render($action, $args = array()) {
        $layout = isset($args['layout']) ? $args['layout'] : null;

        if ($this->rendered) { // every controller should only be rendered once.
            // we only render with whatever output we have caught from processing action here.
            // no template is involved.
            $this->render_with_layout($action, $this->action_output, false);
            return;
        }

        // does the template with the same name as the action exist?
        $underscored_classname = Inflector::underscore(get_class($this));
        $file = APP_VIEWS.DS.substr($underscored_classname, 0, strlen($underscored_classname) - 11) . DS . $action . '.php';
        if ($file === null || !file_exists($file)) {
            Logger::notice("Template [$file] wasn't found.");
            $this->render_with_layout($action, $this->action_output, $layout);
            return;
        }

        ob_start();
        include($file);
        $this->action_output .= ob_get_contents();
        ob_end_clean();

        $this->render_with_layout($action, $this->action_output, $layout);

        $this->rendered = true;
    }

    protected function render_with_layout($action, $content, $layout) {

        if ($layout === false) {
            echo $content;
            return;
        }

        if ($layout !== null) {
            // don't do anything here, since a custom layout is passed in
        } else {
            // check if there is a layout setup for this action already
            $layout = isset($this->layout[$action]) ? $this->layout[$action] : null;

            if ($layout === null) {
                $layout = isset($this->layout[self::DEFAULT_LAYOUT]) ? $this->layout[self::DEFAULT_LAYOUT] : null;
            }

            if ($layout === false) { // default layout could still be specified as false
                echo $content;
                return;
            }

            if ($layout === null ) { // if user has unset the DEFAULT_LAYOUT or set it to null
                // then we fallback to the default behavior.
                $layout = $this->get_default_layout();
            }
        }

        $path = APP_LAYOUTS.DS.$layout.'.php';

        // if the latout file doesn't exist, just render the plain content
        if (!file_exists($path)) {
            echo $content;
            return;
        }

        include($path);

    }

    protected function redirect_to($url) {
        header("Location: ".$url);
    }

    private function get_default_layout() {
        return substr(get_class($this), 0, -10);
    }
}
